package description

import (
	"crypto/rand"
	"encoding/binary"
	"fmt"
	"net/url"
	"strings"
	"time"

	psdp "github.com/pion/sdp/v3"
	"gogs.smarteye.com/smarteye/gortc/pkg/sdp"
)

func newSessionID() (uint64, error) {
	// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-26#section-5.2.1
	// Session ID is recommended to be constructed by generating a 64-bit
	// quantity with the highest bit set to zero and the remaining 63-bits
	// being cryptographically random.
	id, err := cryptoUint64()
	return id & (^(uint64(1) << 63)), err
}

// cryptoUint64 returns cryptographic random uint64.
func cryptoUint64() (uint64, error) {
	var v uint64
	if err := binary.Read(rand.Reader, binary.LittleEndian, &v); err != nil {
		return 0, err
	}
	return v, nil
}

func atLeastOneHasMID(medias []*Media) bool {
	for _, media := range medias {
		if media.ID != "" {
			return true
		}
	}
	return false
}

func atLeastOneDoesntHaveMID(medias []*Media) bool {
	for _, media := range medias {
		if media.ID == "" {
			return true
		}
	}
	return false
}

func hasMediaWithID(medias []*Media, id string) bool {
	for _, media := range medias {
		if media.ID == id {
			return true
		}
	}
	return false
}

// SessionFECGroup is a FEC group.
type SessionFECGroup []string

// Session is the description of a RTSP stream.
type Session struct {
	// Base URL of the stream (read only).
	URI *url.URL

	// Title of the stream (optional).
	Title string

	TimeDescriptions []psdp.TimeDescription

	// FEC groups (RFC5109).
	FECGroups []SessionFECGroup

	// Media streams.
	Medias []*Media
}

// FindFormat finds a certain format among all the formats in all the medias of the stream.
// If the format is found, it is inserted into forma, and its media is returned.
func (d *Session) FindFormat(forma interface{}) *Media {
	for _, media := range d.Medias {
		ok := media.FindFormat(forma)
		if ok {
			return media
		}
	}
	return nil
}

// Unmarshal decodes the description from SDP.
func (d *Session) Unmarshal(ssd *sdp.SessionDescription) error {
	d.URI = ssd.URI
	d.Title = string(ssd.SessionName)
	if d.Title == " " {
		d.Title = ""
	}

	d.Medias = make([]*Media, len(ssd.MediaDescriptions))

	for i, md := range ssd.MediaDescriptions {
		var m Media
		err := m.Unmarshal(md)
		if err != nil {
			return fmt.Errorf("media %d is invalid: %w", i+1, err)
		}

		if m.ID != "" && hasMediaWithID(d.Medias[:i], m.ID) {
			return fmt.Errorf("duplicate media IDs")
		}

		d.Medias[i] = &m
	}

	if atLeastOneHasMID(d.Medias) && atLeastOneDoesntHaveMID(d.Medias) {
		return fmt.Errorf("media IDs sent partially")
	}

	for _, attr := range ssd.Attributes {
		if attr.Key == "group" && strings.HasPrefix(attr.Value, "FEC ") {
			group := SessionFECGroup(strings.Split(attr.Value[len("FEC "):], " "))

			for _, id := range group {
				if !hasMediaWithID(d.Medias, id) {
					return fmt.Errorf("FEC group points to an invalid media ID: %v", id)
				}
			}

			d.FECGroups = append(d.FECGroups, group)
		}
	}

	return nil
}

func (d *Session) Marshal() *sdp.SessionDescription {
	var sessionName psdp.SessionName
	if d.Title != "" {
		sessionName = psdp.SessionName(d.Title)
	} else {
		// RFC 4566: If a session has no meaningful name, the
		// value "s= " SHOULD be used (i.e., a single space as the session name).
		sessionName = psdp.SessionName("-")
	}

	sid, _ := newSessionID()
	sout := &sdp.SessionDescription{
		URI:         d.URI,
		SessionName: sessionName,
		Origin: psdp.Origin{
			Username:       "-",
			SessionID:      sid,
			SessionVersion: uint64(time.Now().Unix()),
			NetworkType:    "IN",
			AddressType:    "IP4",
			UnicastAddress: "0.0.0.0",
		},
		TimeDescriptions:  d.TimeDescriptions,
		Attributes:        []psdp.Attribute{},
		MediaDescriptions: []*psdp.MediaDescription{},
	}

	for _, media := range d.Medias {
		sout.MediaDescriptions = append(sout.MediaDescriptions, media.Marshal())
	}

	for _, group := range d.FECGroups {
		sout.Attributes = append(sout.Attributes, psdp.Attribute{
			Key:   "group",
			Value: "FEC " + strings.Join(group, " "),
		})
	}

	return sout
}

func (d *Session) UnmarshalBytes(data []byte) error {
	var ssd sdp.SessionDescription
	err := ssd.Unmarshal(data)
	if err != nil {
		return err
	}

	return d.Unmarshal(&ssd)
}

// MarshalBytes encodes the description in SDP.
func (d *Session) MarshalBytes() ([]byte, error) {
	return d.Marshal().Marshal()
}
